/*
 *  This file is part of AndroidIDE.
 *
 *  AndroidIDE is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  AndroidIDE is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *   along with AndroidIDE.  If not, see <https://www.gnu.org/licenses/>.
 */

/*
 * Copyright 2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.itsaky.androidide.plugins.tasks;

import com.itsaky.androidide.plugins.tasks.internal.WrapperDefaults;
import com.itsaky.androidide.plugins.tasks.internal.WrapperGenerator;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import javax.annotation.Nullable;
import javax.inject.Inject;
import kotlin.io.FilesKt;
import org.gradle.api.GradleException;
import org.gradle.api.Incubating;
import org.gradle.api.Project;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.internal.file.FileLookup;
import org.gradle.api.internal.file.FileOperations;
import org.gradle.api.internal.file.FileResolver;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.options.Option;
import org.gradle.api.tasks.options.OptionValues;
import org.gradle.api.tasks.wrapper.Wrapper.DistributionType;
import org.gradle.api.tasks.wrapper.Wrapper.PathBase;
import org.gradle.api.tasks.wrapper.WrapperVersionsResources;
import org.gradle.api.tasks.wrapper.internal.DefaultWrapperVersionsResources;
import org.gradle.util.GradleVersion;
import org.gradle.util.internal.GUtil;
import org.gradle.util.internal.WrapperDistributionUrlConverter;
import org.gradle.wrapper.Download;
import org.gradle.wrapper.Logger;
import org.gradle.wrapper.WrapperExecutor;

/**
 * <p>Generates scripts (for *nix and windows) which allow you to build your project with Gradle,
 * without having to install Gradle.
 *
 * <p>When a user executes a wrapper script the first time, the script downloads and installs the
 * appropriate Gradle distribution and runs the build against this downloaded distribution. Any
 * installed Gradle distribution is ignored when using the wrapper scripts.
 *
 * <p>The scripts generated by this task are intended to be committed to your version control
 * system. This task also generates a small {@code gradle-wrapper.jar} bootstrap JAR file and
 * properties file which should also be committed to your VCS. The scripts delegates to this JAR.
 */
public class IDEWrapperGenerator {

  public static final String DEFAULT_DISTRIBUTION_PARENT_NAME = WrapperDefaults.DISTRIBUTION_PATH;

  private File scriptFile = new File(WrapperDefaults.SCRIPT_PATH);
  private File jarFile = new File(WrapperDefaults.JAR_FILE_PATH);
  private String distributionPath = DEFAULT_DISTRIBUTION_PARENT_NAME;
  private PathBase distributionBase = WrapperDefaults.DISTRIBUTION_BASE;
  private String distributionUrl;
  private String distributionSha256Sum;
  private final GradleVersionResolver gradleVersionResolver = new GradleVersionResolver();
  private DistributionType distributionType = WrapperDefaults.DISTRIBUTION_TYPE;
  private String archivePath = WrapperDefaults.ARCHIVE_PATH;
  private PathBase archiveBase = WrapperDefaults.ARCHIVE_BASE;
  private final int networkTimeout = WrapperDefaults.NETWORK_TIMEOUT;
  private boolean distributionUrlConfigured = false;

  void generate(Project project) {
    final var isOffline = project.getGradle().getStartParameter().isOffline();
    File jarFileDestination = getJarFile();
    File unixScript = getScriptFile();
    File propertiesFile = getPropertiesFile();
    Properties existingProperties =
        propertiesFile.exists() ? GUtil.loadProperties(propertiesFile) : null;

    checkProperties(existingProperties);
    validateDistributionUrl(propertiesFile.getParentFile(), isOffline);

    WrapperGenerator.generate(archiveBase, archivePath, distributionBase, distributionPath,
        getDistributionSha256Sum(existingProperties), propertiesFile, jarFileDestination,
        FilesKt.relativeTo(jarFileDestination, unixScript.getParentFile()).getPath(), unixScript, getBatchScript(), getDistributionUrl(),
        getValidateDistributionUrl(), networkTimeout);
  }

  private void checkProperties(Properties existingProperties) {
    String checksumProperty = existingProperties != null ? existingProperties.getProperty(
        WrapperExecutor.DISTRIBUTION_SHA_256_SUM, null) : null;

    if (!isCurrentVersion() && distributionSha256Sum == null && checksumProperty != null) {
      throw new GradleException(
          "gradle-wrapper.properties contains distributionSha256Sum property, but the wrapper configuration does not have one. Specify one in the wrapper task configuration or with the --gradle-distribution-sha256-sum task option");
    }
  }

  private static final String DISTRIBUTION_URL_EXCEPTION_MESSAGE = "Test of distribution url %s failed. Please check the values set with --gradle-distribution-url and --gradle-version.";

  private void validateDistributionUrl(File uriRoot, boolean isOffline) {
    if (distributionUrlConfigured && getValidateDistributionUrl()) {
      String url = getDistributionUrl();
      URI uri = getDistributionUri(uriRoot, url);
      if (uri.getScheme().equals("file")) {
        if (!Files.exists(Paths.get(uri).toAbsolutePath())) {
          throw new UncheckedIOException(String.format(DISTRIBUTION_URL_EXCEPTION_MESSAGE, url));
        }
      } else if (uri.getScheme().startsWith("http") && !isOffline) {
        try {
          new Download(new Logger(true), "gradlew", Download.UNKNOWN_VERSION).sendHeadRequest(uri);
        } catch (Exception e) {
          throw new UncheckedIOException(String.format(DISTRIBUTION_URL_EXCEPTION_MESSAGE, url), e);
        }
      }
    }
  }

  private static URI getDistributionUri(File uriRoot, String url) {
    try {
      return WrapperDistributionUrlConverter.convertDistributionUrl(url, uriRoot);
    } catch (URISyntaxException e) {
      throw new GradleException("Distribution URL String cannot be parsed: " + url, e);
    }
  }

  private String getDistributionSha256Sum(Properties existingProperties) {
    if (distributionSha256Sum != null) {
      return distributionSha256Sum;
    } else if (isCurrentVersion() && existingProperties != null) {
      return existingProperties.getProperty(WrapperExecutor.DISTRIBUTION_SHA_256_SUM, null);
    } else {
      return null;
    }
  }

  private boolean isCurrentVersion() {
    return GradleVersion.current().equals(gradleVersionResolver.getGradleVersion());
  }

  /**
   * Returns the file to write the wrapper script to.
   */

  public File getScriptFile() {
    return scriptFile;
  }

  /**
   * The file to write the wrapper script to.
   *
   * @since 4.0
   */
  public void setScriptFile(File scriptFile) {
    this.scriptFile = scriptFile;
  }

  /**
   * Returns the file to write the wrapper batch script to.
   */

  public File getBatchScript() {
    return WrapperGenerator.getBatchScript(getScriptFile());
  }

  /**
   * Returns the file to write the wrapper jar file to.
   */

  public File getJarFile() {
    return this.jarFile;
  }

  /**
   * The file to write the wrapper jar file to.
   *
   * @since 4.0
   */
  public void setJarFile(File jarFile) {
    this.jarFile = jarFile;
  }

  /**
   * Returns the file to write the wrapper properties to.
   */

  public File getPropertiesFile() {
    return WrapperGenerator.getPropertiesFile(getJarFile());
  }

  /**
   * Returns the path where the gradle distributions needed by the wrapper are unzipped. The path is
   * relative to the distribution base directory
   *
   * @see #setDistributionPath(String)
   */

  public String getDistributionPath() {
    return distributionPath;
  }

  /**
   * Sets the path where the gradle distributions needed by the wrapper are unzipped. The path is
   * relative to the distribution base directory
   *
   * @see #setDistributionPath(String)
   */
  public void setDistributionPath(String distributionPath) {
    this.distributionPath = distributionPath;
  }

  /**
   * Set Wrapper versions resources.
   *
   * @since 8.1
   */
  @Incubating
  public void setWrapperVersionsResources(WrapperVersionsResources wrapperVersionsResources) {
    DefaultWrapperVersionsResources defaultWrapperVersionsResources = (DefaultWrapperVersionsResources) wrapperVersionsResources;
    gradleVersionResolver.setTextResources(defaultWrapperVersionsResources.getLatest(),
        defaultWrapperVersionsResources.getReleaseCandidate(),
        defaultWrapperVersionsResources.getNightly(),
        defaultWrapperVersionsResources.getReleaseNightly());
  }

  /**
   * Returns the gradle version for the wrapper.
   *
   * @throws GradleException if the label that can be provided via {@link #setGradleVersion(String)}
   *                         can not be resolved at the moment. For example, there is not a
   *                         `release-candidate` available at all times.
   * @see #setGradleVersion(String)
   */

  public String getGradleVersion() {
    return gradleVersionResolver.getGradleVersion().getVersion();
  }

  /**
   * The version of the gradle distribution required by the wrapper. This is usually the same
   * version of Gradle you use for building your project. The following labels are allowed to
   * specify a version: {@code latest}, {@code release-candidate}, {@code nightly}, and
   * {@code release-nightly}
   *
   * <p>The resulting distribution url is validated before it is written to the
   * gradle-wrapper.properties file.
   */
  @Option(option = "gradle-version", description =
      "The version of the Gradle distribution required by the wrapper. "
          + "The following labels are allowed: latest, release-candidate, nightly, and release-nightly.")
  public void setGradleVersion(String gradleVersion) {
    distributionUrlConfigured = true;
    this.gradleVersionResolver.setGradleVersionString(gradleVersion);
  }

  /**
   * Returns the type of the Gradle distribution to be used by the wrapper.
   *
   * @see #setDistributionType(DistributionType)
   */

  public DistributionType getDistributionType() {
    return distributionType;
  }

  /**
   * The type of the Gradle distribution to be used by the wrapper. By default, this is
   * {@link DistributionType#BIN}, which is the binary-only Gradle distribution without
   * documentation.
   *
   * @see DistributionType
   */
  @Option(option = "distribution-type", description = "The type of the Gradle distribution to be used by the wrapper.")
  public void setDistributionType(DistributionType distributionType) {
    this.distributionType = distributionType;
  }

  /**
   * The list of available gradle distribution types.
   */
  @OptionValues("distribution-type")
  public List<DistributionType> getAvailableDistributionTypes() {
    return Arrays.asList(DistributionType.values());
  }

  /**
   * The URL to download the gradle distribution from.
   *
   * <p>If not set, the download URL is the default for the specified {@link #getGradleVersion()}.
   *
   * <p>If {@link #getGradleVersion()} is not set, will return null.
   *
   * <p>The wrapper downloads a certain distribution only once and caches it. If your distribution
   * base is the project, you might submit the distribution to your version control system. That way
   * no download is necessary at all. This might be in particular interesting, if you provide a
   * custom gradle snapshot to the wrapper, because you don't need to provide a download server
   * then.
   */

  public String getDistributionUrl() {
    if (distributionUrl != null) {
      return distributionUrl;
    }

    return WrapperGenerator.getDistributionUrl(gradleVersionResolver.getGradleVersion(),
        distributionType);
  }

  /**
   * The URL to download the gradle distribution from.
   *
   * <p>If not set, the download URL is the default for the specified {@link #getGradleVersion()}.
   *
   * <p>If {@link #getGradleVersion()} is not set, will return null.
   *
   * <p>The wrapper downloads a certain distribution and caches it. If your distribution base is
   * the project, you might submit the distribution to your version control system. That way no
   * download is necessary at all. This might be in particular interesting, if you provide a custom
   * gradle snapshot to the wrapper, because you don't need to provide a download server then.
   *
   * <p>The distribution url is validated before it is written to the gradle-wrapper.properties
   * file.
   */
  @Option(option = "gradle-distribution-url", description = "The URL to download the Gradle distribution from.")
  public void setDistributionUrl(String url) {
    distributionUrlConfigured = true;
    this.distributionUrl = url;
  }

  /**
   * The SHA-256 hash sum of the gradle distribution.
   *
   * <p>If not set, the hash sum of the gradle distribution is not verified.
   *
   * <p>The wrapper allows for verification of the downloaded Gradle distribution via SHA-256 hash
   * sum comparison. This increases security against targeted attacks by preventing a
   * man-in-the-middle attacker from tampering with the downloaded Gradle distribution.
   *
   * @since 4.5
   */
  @Nullable
  @Optional

  public String getDistributionSha256Sum() {
    return distributionSha256Sum;
  }

  /**
   * The SHA-256 hash sum of the gradle distribution.
   *
   * <p>If not set, the hash sum of the gradle distribution is not verified.
   *
   * <p>The wrapper allows for verification of the downloaded Gradle distribution via SHA-256 hash
   * sum comparison. This increases security against targeted attacks by preventing a
   * man-in-the-middle attacker from tampering with the downloaded Gradle distribution.
   *
   * @since 4.5
   */
  @Option(option = "gradle-distribution-sha256-sum", description = "The SHA-256 hash sum of the gradle distribution.")
  public void setDistributionSha256Sum(@Nullable String distributionSha256Sum) {
    this.distributionSha256Sum = distributionSha256Sum;
  }

  /**
   * The distribution base specifies whether the unpacked wrapper distribution should be stored in
   * the project or in the gradle user home dir.
   */

  public PathBase getDistributionBase() {
    return distributionBase;
  }

  /**
   * The distribution base specifies whether the unpacked wrapper distribution should be stored in
   * the project or in the gradle user home dir.
   */
  public void setDistributionBase(PathBase distributionBase) {
    this.distributionBase = distributionBase;
  }

  /**
   * Returns the path where the gradle distributions archive should be saved (i.e. the parent dir).
   * The path is relative to the archive base directory.
   */

  public String getArchivePath() {
    return archivePath;
  }

  /**
   * Set's the path where the gradle distributions archive should be saved (i.e. the parent dir).
   * The path is relative to the parent dir specified with {@link #getArchiveBase()}.
   */
  public void setArchivePath(String archivePath) {
    this.archivePath = archivePath;
  }

  /**
   * The archive base specifies whether the unpacked wrapper distribution should be stored in the
   * project or in the gradle user home dir.
   */

  public PathBase getArchiveBase() {
    return archiveBase;
  }

  /**
   * The archive base specifies whether the unpacked wrapper distribution should be stored in the
   * project or in the gradle user home dir.
   */
  public void setArchiveBase(PathBase archiveBase) {
    this.archiveBase = archiveBase;
  }

  /**
   * The network timeout specifies how many ms to wait for when the wrapper is performing network
   * operations, such as downloading the wrapper jar.
   *
   * @since 7.6
   */

  @Incubating
  @Optional
  @Option(option = "network-timeout", description = "Timeout in ms to use when the wrapper is performing network operations.")
  public Integer getNetworkTimeout() {
    return networkTimeout;
  }

  /**
   * Indicates if this task will validate the distribution url that has been configured.
   *
   * @return whether this task will validate the distribution url
   * @since 8.2
   */
  @Incubating

  @Option(option = "validate-url", description = "Sets task to validate the configured distribution url.")
  public Boolean getValidateDistributionUrl() {
    return true;
  }

  @Inject
  protected FileLookup getFileLookup() {
    throw new UnsupportedOperationException();
  }
}

